【systemd】Pythonでstopを検知する&例外時に自動で再起動する
カフェチームの山本です。
現在カフェで使用している機器(Jetson Xavier NX)では、OS起動時にプログラムを自動実行するためにsystemdを利用しています。
今回は、プログラムを終了する際に起きた問題点とその解決方法、また、それらを含めたエラー処理についてまとめました。プログラムはPythonで実装されています。
問題点
systemdでプログラムを起動すると、バックグラウンドプロセスになってしまい、ctrl + Cなどの操作ができません。そのため、サービスを止めるにはsystemdのstopコマンド(sudo systemctl stop SERVICE_NAME)を利用する必要があります。
しかし、stopコマンドを利用した場合、プログラムのプロセスに送られるシグナル(SIGTERM)は通常の中断シグナル(SIGKILL)と異なるため、Pythonプログラムそのままだと捕捉できず、except節が実行されません(これにより、プログラム終了時にリソースの開放などができず、再度起動した際にエラーになる場合があります)。
実現したい動作
上記の問題の解決に加え、プログラムで起きたエラー対処として、以下のように処理する必要がありました。次節でこの実装方法について述べます。
- (1):ctrl + C (KeyIntterupt)→ リソースを開放し、通常通り終了する
- (2):systemd stop → リソースを開放し、通常通り終了する
- (3):その他のエラー(既知) → 個別に対処する
- (4):その他のエラー(未知) → エラー(例外)として処理し、プログラムを再起動する
実装したコード
以下のように、2ファイルを実装(設定)しました。
.serviceファイル
systemdに登録するサービスの設定ファイルです。
[Unit] Description=service to lanuch skeleton detection program on jetson [Service] WorkingDirectory=/home/USER/ ExecStart=/bin/sh /home/USER/launch_jetson_program.sh User=USER Restart=on-failure [Install] WantedBy=default.target
Restart=on-failureを付け加えることで、プログラムがエラーで終了した際に、systemdが自動でプログラムを再起動するようになります。
詳しくは、以下をご参照ください。
Ubuntu Manpage: systemd.service - Service unit configuration
Pythonスクリプト
プログラムを以下のように実装しました("# (preprocess)" や " # (execute process)" は、プログラムの処理が書かれていた箇所ですが、今回は解説のために省略しています)。今回は、動作(3)として、ネットワークエラーが起きた際の処理を書きました。
import signal import socket class TerminatedExecption(Exception): pass def raise_exception(*_): raise TerminatedExecption() if __name__ == "__main__": # set signal handler to detect to be stopped by systemd signal.signal(signal.SIGTERM, raise_exception) # (preprocess) # execute try: # (execute process) # (1) if ctrl-C is pushed, stop program nomally except KeyboardInterrupt: print("KeyboardInterrupt: stopped by keyboard input (ctrl-C)") # (2) if stopped by systemd, stop program nomally except TerminatedExecption: print("TerminatedExecption: stopped by systemd") # (3) if error is caused with network, restart program by systemd except OSError as e: import traceback traceback.print_exc() print("NETWORK_ERROR") # program will be restarted automatically by systemd (Restart on-failure) raise e # (4) if other error, restart program by systemd except Exception as e: import traceback traceback.print_exc() print("UNKNOWN_ERROR") # program will be restarted automatically by systemd (Restart on-failure) raise e
stopコマンド(SIGTERM)への対応
main関数の最初で、SIGTERMを受信した際の動作を、raise_exception関数を呼び、TerminatedExecptionをraiseするように変更しました。これにより、stopコマンドを例外として処理できるようになりました。
各エラー処理への対応
- 動作 (1)・(2):受け取った例外をraiseせずに終了します。これにより、プログラムが正常終了するため、systemdはプログラムを再起動しません(開発中など、意図的にプログラムを止めた場合に再起動すると不便なため、このような設定にしました)。
- 動作 (3)・(4):受け取った例外をraiseします。これにより、プログラムがエラーで終了するため、systemdはプログラムを再起動します。
まとめ
systemdを利用した場合、Pythonのsignal処理の変更することで、stopコマンドを例外として処理できるようになりました。また、serviceファイルのRestartを利用することで、エラー時にプログラムを再起動するようになりました。
参考にさせていただいたページ・サイト
Ubuntu Manpage: systemd.service - Service unit configuration
systemdでstopした時にpythonのfinallyが呼ばれない問題について - dothikoのカクカクワールド2D REBOOT